#include "fastcommon/php7_ext_wrapper.h"
#include "ext/standard/info.h"
#include <zend_extensions.h>
#include <zend_exceptions.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <time.h>
#include "fastcommon/common_define.h"
#include "fastcommon/logger.h"
#include "fastken/fken_client.h"
#include "fkenclient.h"

#define MAJOR_VERSION  1
#define MINOR_VERSION  0
#define PATCH_VERSION  0

typedef struct {
    FKenClient client;
    int flags;
} PHPFKenClient;

#define DEFAULT_CONFIG_FILENAME  "/etc/fken/client.conf"
#define PHP_FKENCLIENT_RESOURCE_NAME "fkenclient"
static int le_consumer;

static PHPFKenClient *last_client = NULL;

#if (PHP_MAJOR_VERSION == 5 && PHP_MINOR_VERSION < 3)
const zend_fcall_info empty_fcall_info = { 0, NULL, NULL, NULL, NULL, 0, NULL, NULL, 0 };
#undef ZEND_BEGIN_ARG_INFO_EX
#define ZEND_BEGIN_ARG_INFO_EX(name, pass_rest_by_reference, return_reference, required_num_args) \
    static zend_arg_info name[] = {                                                               \
        { NULL, 0, NULL, 0, 0, 0, pass_rest_by_reference, return_reference, required_num_args },
#endif

// Every user visible function must have an entry in fkenclient_functions[].
	zend_function_entry fkenclient_functions[] = {
		ZEND_FE(fkenclient_version, NULL)
		ZEND_FE(fkenclient_init, NULL)
		ZEND_FE(fkenclient_destroy, NULL)
		ZEND_FE(fkenclient_question_search, NULL)
		{NULL, NULL, NULL}  /* Must be the last line */
	};

zend_module_entry fkenclient_module_entry = {
	STANDARD_MODULE_HEADER,
	"fkenclient",
	fkenclient_functions,
	PHP_MINIT(fkenclient),
	PHP_MSHUTDOWN(fkenclient),
	NULL,//PHP_RINIT(fkenclient),
	NULL,//PHP_RSHUTDOWN(fkenclient),
	PHP_MINFO(fkenclient),
	"1.00",
	STANDARD_MODULE_PROPERTIES
};

#ifdef COMPILE_DL_FKENCLIENT
	ZEND_GET_MODULE(fkenclient)
#endif

ZEND_RSRC_DTOR_FUNC(fkenclient_resource_dtor)
{
    PHPFKenClient *php_client = NULL;

#if PHP_MAJOR_VERSION < 7
    if (rsrc->ptr != NULL)
    {
        php_client = (PHPFKenClient *)rsrc->ptr;
        rsrc->ptr = NULL;
    }
#else
    if (res->ptr != NULL)
    {
        php_client = (PHPFKenClient *)res->ptr;
        res->ptr = NULL;
    }
#endif

    if (php_client != NULL) {
        fken_client_destroy(&php_client->client);
        if (last_client == php_client)
        {
            last_client = NULL;
        }
        efree(php_client);
    }
}

PHP_MINIT_FUNCTION(fkenclient)
{
    log_try_init();
    le_consumer = zend_register_list_destructors_ex(fkenclient_resource_dtor,
            NULL, PHP_FKENCLIENT_RESOURCE_NAME, module_number);

    REGISTER_LONG_CONSTANT("FKEN_ANSWER_FORMAT_TEXT",
            FKEN_ANSWER_FORMAT_TEXT, CONST_CS | CONST_PERSISTENT);

    REGISTER_LONG_CONSTANT("FKEN_ANSWER_FORMAT_HTML",
            FKEN_ANSWER_FORMAT_HTML, CONST_CS | CONST_PERSISTENT);

    return SUCCESS;
}

PHP_MSHUTDOWN_FUNCTION(fkenclient)
{
	return SUCCESS;
}

PHP_RINIT_FUNCTION(fkenclient)
{
	return SUCCESS;
}

PHP_RSHUTDOWN_FUNCTION(fkenclient)
{
	return SUCCESS;
}

PHP_MINFO_FUNCTION(fkenclient)
{
	char buffer[64];
	sprintf(buffer, "fkenclient v%d.%02d support", 
		MAJOR_VERSION, MINOR_VERSION);

	php_info_print_table_start();
	php_info_print_table_header(2, buffer, "enabled");
	php_info_print_table_end();
}

/*
string fkenclient_version()
return client library version
*/
ZEND_FUNCTION(fkenclient_version)
{
	char szVersion[16];
	int len;

	len = sprintf(szVersion, "%d.%d.%d",
		MAJOR_VERSION, MINOR_VERSION, PATCH_VERSION);

	ZEND_RETURN_STRINGL(szVersion, len, 1);
}

/*
resource fkenclient_init([string config_filename = "/etc/fken/client.conf"])
return resource handle for success, false for fail
*/
ZEND_FUNCTION(fkenclient_init)
{
    int argc;
    zend_size_t question_len;
    char *config_filename;
    PHPFKenClient *php_client;

	argc = ZEND_NUM_ARGS();
	if (argc > 1) {
		logError("file: "__FILE__", line: %d, "
			"fkenclient_init parameters count: %d is invalid",
			__LINE__, argc);
		RETURN_BOOL(false);
	}

	config_filename = DEFAULT_CONFIG_FILENAME;
	if (zend_parse_parameters(argc TSRMLS_CC, "|s", &config_filename,
                &question_len) == FAILURE)
	{
		logError("file: "__FILE__", line: %d, "
			"zend_parse_parameters fail!", __LINE__);
		RETURN_BOOL(false);
	}

    php_client = (PHPFKenClient *)emalloc(sizeof(PHPFKenClient));
    if (php_client == NULL) {
		logError("file: "__FILE__", line: %d, "
			"emalloc %d bytes fail!", __LINE__, (int)sizeof(PHPFKenClient));
		RETURN_BOOL(false);
    }

    if (fken_client_init(&php_client->client, config_filename) != 0) {
		RETURN_BOOL(false);
	}

    last_client = php_client;
    ZEND_REGISTER_RESOURCE(return_value, php_client, le_consumer);
}

/*
bool fkenclient_destroy([resource handle = NULL])
return true for success, false for fail
*/
ZEND_FUNCTION(fkenclient_destroy)
{
    int argc;
    zval *zhandle;
    PHPFKenClient *php_client;
    FKenClient *client;

	argc = ZEND_NUM_ARGS();
	if (argc > 1) {
		logError("file: "__FILE__", line: %d, "
			"fkenclient_destroy parameters count: %d is invalid",
			__LINE__, argc);
		RETURN_BOOL(false);
	}

    zhandle = NULL;
	if (zend_parse_parameters(argc TSRMLS_CC, "|z", &zhandle) == FAILURE) {
		logError("file: "__FILE__", line: %d, "
			"zend_parse_parameters fail!", __LINE__);
		RETURN_BOOL(false);
	}

    if (zhandle != NULL && !ZVAL_IS_NULL(zhandle)) {
        if (ZEND_TYPE_OF(zhandle) != IS_RESOURCE) {
            logError("file: "__FILE__", line: %d, "
                    "invalid fkenclient handle!", __LINE__);
            RETURN_BOOL(false);
        }
        ZEND_FETCH_RESOURCE(php_client, PHPFKenClient *, &zhandle, -1,
                PHP_FKENCLIENT_RESOURCE_NAME, le_consumer);
        client = &php_client->client;
    } else {
        if (last_client == NULL) {
            logError("file: "__FILE__", line: %d, "
                    "must call fkenclient_init first", __LINE__);
            RETURN_BOOL(false);
        }
        client = &last_client->client;
        last_client = NULL;
    }

	fken_client_destroy(client);
	RETURN_BOOL(true);
}

static int ht_to_kv_array(HashTable *ht, key_value_pair_t *kvs, int *count)
{
	unsigned long index;
	HashPosition pointer;
	zval **data;
#if PHP_MAJOR_VERSION < 7
	zval ***ppp;
#else
	zval *for_php7;
#endif
    static char buff[256];
    char *p;
    key_value_pair_t *curkv;

    p = buff;
    curkv = kvs;
#if PHP_MAJOR_VERSION < 7
	ppp = &data;
	for (zend_hash_internal_pointer_reset_ex(ht, &pointer);
		zend_hash_get_current_data_ex(ht, (void **)ppp, &pointer)
		 == SUCCESS; zend_hash_move_forward_ex(ht, &pointer))
#else
	data = &for_php7;
	for (zend_hash_internal_pointer_reset_ex(ht, &pointer);
		(for_php7=zend_hash_get_current_data_ex(ht, &pointer))
		 != NULL; zend_hash_move_forward_ex(ht, &pointer))
#endif
	{
#if PHP_MAJOR_VERSION < 7
		if (zend_hash_get_current_key_ex(ht, &curkv->key.str,
			 &(curkv->key.len), &index, 0, &pointer) != HASH_KEY_IS_STRING)
#else
		zend_string *_key_ptr;
		if (zend_hash_get_current_key_ex(ht, &_key_ptr,
			 (zend_ulong *)&index, &pointer) != HASH_KEY_IS_STRING)
#endif
		{
			logError("file: "__FILE__", line: %d, "
				"invalid array element, "
				"index=%ld!", __LINE__, index);
			return EINVAL;
		}

#if PHP_MAJOR_VERSION >= 7
		curkv->key.str = _key_ptr->val;
		curkv->key.len = _key_ptr->len;
#endif

		if (ZEND_TYPE_OF(*data) == IS_STRING) {
			curkv->value.str = Z_STRVAL_PP(data);
			curkv->value.len = Z_STRLEN_PP(data);
		} else if (ZEND_TYPE_OF(*data) == IS_LONG || ZEND_IS_BOOL(*data)) {
            curkv->value.str = p;
			curkv->value.len = sprintf(p, "%ld", (*data)->value.lval);
            p += curkv->value.len + 1;
		} else if (ZEND_TYPE_OF(*data) == IS_DOUBLE) {
            curkv->value.str = p;
			curkv->value.len = sprintf(p, "%.2f", (*data)->value.dval);
            p += curkv->value.len + 1;
		} else if (ZEND_TYPE_OF(*data) == IS_NULL) {
            curkv->value.str = p;
            curkv->value.len = 0;
            *p++ = '\0';
        } else {
            logError("file: "__FILE__", line: %d, "
                    "invalid array element, key=%s, value type=%d",
                    __LINE__, curkv->key.str, ZEND_TYPE_OF(*data));
            return EINVAL;
        }

        curkv++;
    }

    *count = curkv - kvs;
    return 0;
}

static void output_results(FKenAnswerArray *answer_array, zval *rv)
{
    zval *answer_entry_arr;
    FKenAnswerEntry *entry;
    FKenAnswerEntry *end;
    char buff[32];
    string_t id;
    int i;

    id.str = buff;
    array_init(rv);
    end = answer_array->answers + answer_array->count;
    for (entry=answer_array->answers, i=0; entry<end; entry++, i++) {
        ALLOC_INIT_ZVAL(answer_entry_arr);
        array_init(answer_entry_arr);
        add_index_zval(rv, i, answer_entry_arr);

        id.len = sprintf(id.str, "%"PRId64, entry->id);
        zend_add_assoc_stringl_ex(answer_entry_arr, "id", sizeof("id"),
                id.str, id.len, 1);
        zend_add_assoc_stringl_ex(answer_entry_arr, "answer", sizeof("answer"),
                entry->answer.str, entry->answer.len, 1);
    }
}

/*
array fkenclient_question_search(string question, [array vars = NULL,
         int answer_format = FKEN_ANSWER_FORMAT_HTML, resource handle = NULL])
return true for success, false for fail
*/
ZEND_FUNCTION(fkenclient_question_search)
{
	int argc;
    zend_size_t question_len;
    string_t question;
	zval *zvars;
    long answer_format;
    zval *zhandle;
    PHPFKenClient *php_client;
    FKenClient *client;
	HashTable *htvars;
    key_value_pair_t vars[FKEN_MAX_CONDITION_COUNT];
    int var_count;
    FKenAnswerArray answer_array;

	argc = ZEND_NUM_ARGS();
	if (argc < 2) {
		logError("file: "__FILE__", line: %d, "
			"fkenclient_question_search need array parameter",
			__LINE__);
		RETURN_BOOL(false);
	}

    zvars = NULL;
    answer_format = FKEN_ANSWER_FORMAT_HTML;
    zhandle = NULL;
	if (zend_parse_parameters(argc TSRMLS_CC, "s|alz", &question.str,
                &question_len, &zvars, &answer_format, &zhandle) == FAILURE)
    {
		logError("file: "__FILE__", line: %d, "
			"fkenclient_question_search zend_parse_parameters fail!", __LINE__);
		RETURN_BOOL(false);
	}
    question.len = question_len;
    if (zvars != NULL) {
        htvars = Z_ARRVAL_P(zvars);
        var_count = (int)zend_hash_num_elements(htvars);
        if (var_count > FKEN_MAX_CONDITION_COUNT) {
            logError("file: "__FILE__", line: %d, "
                    "too many array elements %d exceeds %d ",
                    __LINE__, var_count, FKEN_MAX_CONDITION_COUNT);
            RETURN_BOOL(false);
        }
        if (ht_to_kv_array(htvars, vars, &var_count) != 0) {
            RETURN_BOOL(false);
        }
    } else {
        var_count = 0;
    }

    if (zhandle != NULL && !ZVAL_IS_NULL(zhandle)) {
        if (ZEND_IS_FALSE(zhandle)) {
            logError("file: "__FILE__", line: %d, "
                    "empty fkenclient handle!", __LINE__);
            RETURN_BOOL(false);
        } else if (ZEND_TYPE_OF(zhandle) != IS_RESOURCE) {
            logError("file: "__FILE__", line: %d, "
                    "invalid fkenclient handle!", __LINE__);
            RETURN_BOOL(false);
        }
        ZEND_FETCH_RESOURCE(php_client, PHPFKenClient *, &zhandle, -1,
                PHP_FKENCLIENT_RESOURCE_NAME, le_consumer);
        client = &php_client->client;
    } else {
        if (last_client == NULL) {
            logError("file: "__FILE__", line: %d, "
                    "must call fkenclient_init first", __LINE__);
            RETURN_BOOL(false);
        }
        client = &last_client->client;
    }

    if (fken_client_question_search(client, &question, vars, var_count,
                answer_format, &answer_array) != 0)
    {
        RETURN_BOOL(false);
    }

    output_results(&answer_array, return_value);
}
